﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
using System.IO;
using System.Xml.Linq;

namespace WebChatApi.Common.LightAccessControl
{
    /// <summary>
    /// Class LightAccessControlManager. This class cannot be inherited.
    /// </summary>
    public sealed class LightAccessControlManager : Singleton<LightAccessControlManager>
    {
        #region Constants

        /// <summary>
        /// The triple DES key length
        /// </summary>
        private const int tripleDesKeyLength = 24;

        /// <summary>
        /// The XML node_ root node
        /// </summary>
        private const string xmlNode_RootNode = "AccessKey";

        /// <summary>
        /// The XML node_ item
        /// </summary>
        private const string xmlNode_Item = "Item";

        /// <summary>
        /// The attribute_ access key
        /// </summary>
        private const string attribute_AccessKey = "AccessKey";

        /// <summary>
        /// The attribute_ secret key
        /// </summary>
        private const string attribute_SecretKey = "SecretKey";

        /// <summary>
        /// The default access license file name
        /// </summary>
        private const string defaultAccessLicenseFileName = "Access.license";

        /// <summary>
        /// The backup access license file name
        /// </summary>
        private const string backupAccessLicenseFileName = "license.licb";

        #endregion

        /// <summary>
        /// Initializes a new instance of the <see cref="LightAccessControlManager"/> class.
        /// </summary>
        public LightAccessControlManager()
        {
            this.LicensePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, defaultAccessLicenseFileName);
            this.BackupLicensePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, backupAccessLicenseFileName);
            Refresh();
        }

        #region Properties

        /// <summary>
        /// The locker
        /// </summary>
        private object locker = new object();

        /// <summary>
        /// The backup locker
        /// </summary>
        private object backupLocker = new object();

        /// <summary>
        /// The access keys
        /// </summary>
        private Dictionary<Guid, AccessLicenseWithSettings> accessLicenses = new Dictionary<Guid, AccessLicenseWithSettings>();

        /// <summary>
        /// Gets or sets the license path.
        /// </summary>
        /// <value>
        /// The license path.
        /// </value>
        public string LicensePath
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets or sets the backup license path.
        /// </summary>
        /// <value>
        /// The backup license path.
        /// </value>
        private string BackupLicensePath
        {
            get;
            set;
        }

        #endregion

        #region Public methods

        /// <summary>
        /// Gets the access licenses.
        /// </summary>
        /// <returns></returns>
        public List<AccessLicenseWithSettings> GetAccessLicenses()
        {
            return GetAccessLicensesWithLocker();
        }

        /// <summary>
        /// Refreshes this instance.
        /// </summary>
        public void Refresh()
        {
            InitializeLicenses();
        }

        /// <summary>
        /// Adds the access license.
        /// </summary>
        /// <returns></returns>
        public AccessLicenseWithSettings AddAccessLicense()
        {
            var result = new AccessLicenseWithSettings { AccessKey = Guid.NewGuid(), SecretKey = GenerateRandomSecretKey() };
            this.AddAccessLicenseWithLocker(result);
            return result;
        }

        /// <summary>
        /// Adds the access license.
        /// </summary>
        /// <param name="accessLicense">The access license.</param>
        public void AddAccessLicense(AccessLicenseWithSettings accessLicense)
        {
            if (accessLicense != null)
            {
                this.AddAccessLicenseWithLocker(accessLicense);

                lock (backupLocker)
                {
                    SaveCurrentSettingsAs(LicensePath);
                    SaveCurrentSettingsAs(BackupLicensePath);
                }
            }
        }

        /// <summary>
        /// Removes the access key.
        /// </summary>
        /// <param name="accessKey">The access key.</param>
        public void RemoveAccessKey(Guid accessKey)
        {
            RemoveAccessLicenseWithLocker(accessKey);
        }

        /// <summary>
        /// Authenticates the access licenses.
        /// </summary>
        /// <param name="accessLicense">The access license.</param>
        /// <param name="operationIdentity">The operation identity.</param>
        /// <returns></returns>
        public AccessLicenseWithSettings AuthenticateAccessLicenses(AccessLicense accessLicense, string operationIdentity)
        {
            if (accessLicense == null)
            {
                throw new UnauthorizedOperationException(operationIdentity, null);
            }

            return AuthenticateAccessLicenses(accessLicense.AccessKey, accessLicense.SecretKey, operationIdentity);
        }

        /// <summary>
        /// Validates the access license.
        /// </summary>
        /// <param name="accessKey">The access key.</param>
        /// <param name="secretKey">The secret key.</param>
        /// <param name="operationIdentity">The operation identity.</param>
        /// <returns></returns>
        /// <exception cref="UnauthorizedOperationException"></exception>
        /// <exception cref="Gbi.UnauthorizedOperationException"></exception>
        public AccessLicenseWithSettings AuthenticateAccessLicenses(Guid accessKey, string secretKey, string operationIdentity)
        {
            AccessLicenseWithSettings result = null;
            result = this.AuthenticateAccessLicensesWithLocker(accessKey, secretKey);

            if (result == null)
            {
                throw new UnauthorizedOperationException(operationIdentity, accessKey.ToString());
            }

            return result;
        }

        #endregion

        #region Locker operations

        /// <summary>
        /// Gets the access licenses with locker.
        /// </summary>
        /// <returns></returns>
        private List<AccessLicenseWithSettings> GetAccessLicensesWithLocker()
        {
            List<AccessLicenseWithSettings> result = new List<AccessLicenseWithSettings>();

            lock (locker)
            {
                foreach (var key in this.accessLicenses.Keys)
                {
                    result.Add(this.accessLicenses[key]);
                }
            }

            return result;
        }

        /// <summary>
        /// Adds the access license with locker.
        /// </summary>
        /// <param name="accessLicense">The access license.</param>
        private void AddAccessLicenseWithLocker(AccessLicenseWithSettings accessLicense)
        {
            if (accessLicense != null && !string.IsNullOrWhiteSpace(accessLicense.SecretKey) && accessLicense.SecretKey.Length > 20)
            {
                lock (locker)
                {
                    this.accessLicenses.Merge(accessLicense.AccessKey, accessLicense);
                }
                lock (backupLocker)
                {
                    SaveCurrentSettingsAs(LicensePath);
                    SaveCurrentSettingsAs(BackupLicensePath);
                }
            }
        }

        /// <summary>
        /// Removes the access license with locker.
        /// </summary>
        /// <param name="accessKey">The access key.</param>
        private void RemoveAccessLicenseWithLocker(Guid accessKey)
        {
            lock (locker)
            {
                if (this.accessLicenses.ContainsKey(accessKey))
                {
                    this.accessLicenses.Remove(accessKey);
                }
            }
        }

        /// <summary>
        /// Loads the access licenses with locker.
        /// </summary>
        /// <param name="xElement">The x element.</param>
        private void LoadAccessLicensesWithLocker(XElement xElement)
        {
            if (xElement != null)
            {
                lock (locker)
                {
                    this.accessLicenses.Clear();

                    if (xElement.Name.LocalName == xmlNode_RootNode)
                    {
                        foreach (var one in xElement.Elements(xmlNode_Item))
                        {

                            string secretKey = DecryptSecretKey(one.GetAttributeValue(attribute_SecretKey));
                            Guid? accessKey = one.GetAttributeValue(attribute_AccessKey).DBToGuid();

                            if (accessKey != null && !string.IsNullOrWhiteSpace(secretKey))
                            {
                                AccessLicenseWithSettings license = new AccessLicenseWithSettings();
                                license.SecretKey = secretKey;
                                license.AccessKey = accessKey.Value;
                                foreach (XElement xml in one.Elements())
                                {
                                    license.Settings.LoadFromXml(xml);
                                    break;
                                }

                                this.accessLicenses.Merge(license.AccessKey, license);
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Authenticates the access licenses with locker.
        /// </summary>
        /// <param name="accessKey">The access key.</param>
        /// <param name="secretKey">The secrect key.</param>
        /// <returns></returns>
        private AccessLicenseWithSettings AuthenticateAccessLicensesWithLocker(Guid accessKey, string secretKey)
        {
            AccessLicenseWithSettings license = null;

            lock (locker)
            {
                if (this.accessLicenses.ContainsKey(accessKey))
                {
                    license = this.accessLicenses[accessKey];

                    if (string.IsNullOrWhiteSpace(secretKey) || !secretKey.Equals(license.SecretKey))
                    {
                        license = null;
                    }
                }
            }

            return license;
        }

        #endregion

        /// <summary>
        /// Saves the current settings as.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        private void SaveCurrentSettingsAs(string filePath)
        {
            var licenses = GetAccessLicensesWithLocker();
            var xElement = ToXml(licenses);
            xElement.Save(filePath);
        }

        /// <summary>
        /// Initializes the licenses.
        /// </summary>
        private void InitializeLicenses()
        {
            lock (backupLocker)
            {
                bool isSuccess = false;
                try
                {
                    if (!File.Exists(LicensePath) && !File.Exists(BackupLicensePath))
                    {
                        SaveCurrentSettingsAs(LicensePath);
                    }
                    else
                    {
                        Load(LicensePath);
                    }
                    isSuccess = true;
                }
                catch
                {
                    try
                    {
                        Load(BackupLicensePath);
                        SaveCurrentSettingsAs(LicensePath);
                    }
                    catch (Exception ex)
                    {
                        throw new InitializationFailureException("InitializeLicenses", ex);
                    }
                }
                finally
                {
                    if (isSuccess)
                    {
                        SaveCurrentSettingsAs(BackupLicensePath);
                    }
                }
            }
        }

        /// <summary>
        /// Loads the specified file path.
        /// </summary>
        /// <param name="filePath">The file path.</param>
        private void Load(string filePath)
        {
            try
            {
                XElement xElement = XElement.Load(filePath);
                LoadAccessLicensesWithLocker(xElement);
            }
            catch (Exception ex)
            {
                throw new InitializationFailureException("LightAccessControlManager", ex, filePath);
            }
        }

        #region Private methods

        /// <summary>
        /// To the XML.
        /// </summary>
        /// <returns></returns>
        private XElement ToXml()
        {
            List<AccessLicenseWithSettings> licenses = new List<AccessLicenseWithSettings>();
            licenses = GetAccessLicenses();
            return ToXml(licenses);
        }

        #endregion

        #region Static private methods

        /// <summary>
        /// Encrypts the secret key.
        /// </summary>
        /// <param name="secretKey">The secret key.</param>
        /// <returns></returns>
        private static string EncryptSecretKey(string secretKey)
        {
            string result = secretKey;
            if (!string.IsNullOrWhiteSpace(secretKey))
            {
                var encoding = Encoding.ASCII;

                byte[] keyBytes = GenerateRandom3DESKey();
                TripleDESCryptoServiceProvider DES = new TripleDESCryptoServiceProvider();

                DES.Key = keyBytes;
                DES.Mode = CipherMode.ECB;
                DES.Padding = System.Security.Cryptography.PaddingMode.PKCS7;

                ICryptoTransform DESEncrypt = DES.CreateEncryptor();

                try
                {
                    byte[] buffer = encoding.GetBytes(secretKey);
                    buffer = DESEncrypt.TransformFinalBlock(buffer, 0, buffer.Length);
                    List<byte> data = new List<byte>(keyBytes);
                    data.AddRange(buffer);
                    result = Convert.ToBase64String(data.ToArray());
                }
                catch { }
            }

            return result;
        }


        /// <summary>
        /// Decrypts the secret key.
        /// </summary>
        /// <param name="secretKey">The secret key.</param>
        /// <returns></returns>
        private static string DecryptSecretKey(string secretKey)
        {
            string result = secretKey;
            if (!string.IsNullOrWhiteSpace(secretKey))
            {
                var encoding = Encoding.ASCII;

                try
                {
                    byte[] buffer = Convert.FromBase64String(secretKey);
                    List<byte> bytes = new List<byte>(buffer);
                    TripleDESCryptoServiceProvider DES = new TripleDESCryptoServiceProvider();

                    DES.Key = bytes.GetRange(0, tripleDesKeyLength).ToArray();
                    buffer = bytes.GetRange(tripleDesKeyLength, bytes.Count - tripleDesKeyLength).ToArray();
                    DES.Mode = CipherMode.ECB;
                    DES.Padding = System.Security.Cryptography.PaddingMode.PKCS7;

                    ICryptoTransform DESDecrypt = DES.CreateDecryptor();
                    buffer = DESDecrypt.TransformFinalBlock(buffer, 0, buffer.Length);
                    result = encoding.GetString(buffer);
                }
                catch { }
            }

            return result;
        }

        /// <summary>
        /// Generates the random3 DES key.
        /// </summary>
        /// <returns></returns>
        private static byte[] GenerateRandom3DESKey()
        {
            return EncodingUtil.Instance.GetRandomHex(tripleDesKeyLength);
        }

        /// <summary>
        /// Generates the random secrect key.
        /// </summary>
        /// <returns></returns>
        private static string GenerateRandomSecretKey()
        {
            return EncodingUtil.Instance.GetRandomString(32);
        }

        /// <summary>
        /// To the XML.
        /// </summary>
        /// <param name="licenses">The licenses.</param>
        /// <returns></returns>
        public static XElement ToXml(ICollection<AccessLicenseWithSettings> licenses)
        {
            XElement result = licenses.CreateXmlNode(xmlNode_RootNode);

            if (licenses != null)
            {
                foreach (var one in licenses)
                {
                    XElement item = licenses.CreateXmlNode(xmlNode_Item);
                    item.SetAttributeValue(attribute_AccessKey, one.AccessKey.DBToString());
                    item.SetAttributeValue(attribute_SecretKey, EncryptSecretKey(one.SecretKey));
                    item.Add(one.Settings.ToXml());

                    result.Add(item);
                }
            }

            return result;
        }

        /// <summary>
        /// Froms the XML.
        /// </summary>
        /// <param name="xElement">The x element.</param>
        /// <returns></returns>
        public static List<AccessLicenseWithSettings> FromXml(XElement xElement)
        {
            List<AccessLicenseWithSettings> result = new List<AccessLicenseWithSettings>();

            if (xElement.Name.LocalName == xmlNode_RootNode)
            {
                foreach (var one in xElement.Elements(xmlNode_Item))
                {

                    string secretKey = DecryptSecretKey(one.GetAttributeValue(attribute_SecretKey));
                    Guid? accessKey = one.GetAttributeValue(attribute_AccessKey).DBToGuid();

                    if (accessKey != null && !string.IsNullOrWhiteSpace(secretKey))
                    {
                        AccessLicenseWithSettings license = new AccessLicenseWithSettings();
                        license.SecretKey = secretKey;
                        license.AccessKey = accessKey.Value;
                        foreach (XElement xml in one.Elements())
                        {
                            license.Settings.LoadFromXml(xml);
                            break;
                        }

                        result.Add(license);
                    }
                }
            }

            return result;
        }

        #endregion
    }
}
